import { _decorator, Component, Node } from 'cc';
import { buildUINodeEx, NodeEx } from './NodeEx';
import { MapGrid } from './MapGrid';
import { compareZIndex, KKP2D, subP2D, GridForm, GridFormRhomboid, GridFormOrthogonal, GridFormHexagonal, KK_ZERO, UNIT_SIZE, KKSize, GridEdge, KKP2DTable } from '@coldcloude/kakera';
import { approxSort, KList, KMap, KNumTree, KStrTable } from '@coldcloude/kai2';
import { MapPosition } from './MapPosition';
import { MapController } from './MapController';
import { TileController } from './TileController';
const { ccclass } = _decorator;

type Tile = {
    node: Node,
    grid: KKP2D,
    size: KKSize,
    edge: GridEdge,
    offset: KKP2D,
    layer: number
}

const EMPTY_EDGE = new KNumTree<{min:number,max:number}>();

function compareLayerOffset(t1:Tile,t2:Tile):number{
    return t1.layer<t2.layer?-1:t1.layer===t2.layer?compareZIndex(t1.offset,t2.offset):1;
}

@ccclass('ScrollMap')
export class ScrollMap extends Component {

    //configs
    form:"orthogonal"|"rhomboid"|"hexagonal";

    viewX: number;
    viewY: number;
    viewWidth: number;
    viewHeight: number;

    tileWidth: number;
    tileHeight: number;
    tileSide:number;

    initGridX: number;
    initGridY: number;

    initX: number;
    initY: number;

    checkSize = false;

    //runtimes

    controller:MapController;

    gridForm:GridForm;

    viewGrid:MapGrid;

    viewPosition:MapPosition;

    groundLayers:number[] = [];

    groundTileNodesMap = new KNumTree<[Node,KMap<KKP2D,Node>]>();

    customNode:NodeEx;

    customTileNodesMap = new KNumTree<KMap<KKP2D,Node>>();

    customObjNodeMap = new KStrTable<Node>();

    _addTileNodes(parent:Node,create:()=>Node,layer:number):KMap<KKP2D,Node>{
        //find grids
        const nodeMap = new KKP2DTable<Node>();
        const grids = this.gridForm.getRectGrids({
            x: this.viewX-(this.viewWidth>>1),
            y: this.viewY-(this.viewHeight>>1)
        },{
            width: this.viewWidth+(this.viewWidth>>1),
            height: this.viewHeight+(this.viewHeight>>1)
        });
        for(const grid of grids){
            //add node
            const node = create();
            parent.addChild(node);
            nodeMap.set(grid,node);
            //set grid
            let mapGrid = node.getComponent(MapGrid);
            if(!mapGrid){
                mapGrid = node.addComponent(MapGrid);
            }
            mapGrid.grid = grid;
            mapGrid.refMap = this.viewGrid;
            mapGrid.layer = layer;
            //set position
            const pixel = this.gridForm.toPixel(grid);
            node.setPosition(pixel.x,pixel.y);
        }
        return nodeMap;
    }

    _triggerTileChange(nodeMap:KMap<KKP2D,Node>){
        nodeMap.foreach((grid,node)=>{
            const tileCtrl = node.getComponent(TileController);
            if(tileCtrl){
                tileCtrl.change();
            }
        });
    }

    addGroundTiles(create:()=>Node,layer:number):[Node,KMap<KKP2D,Node>]{
        return this.groundTileNodesMap.computeIfAbsent(layer,()=>{
            //add panel node
            const panel = create();
            this.groundLayers.push(layer);
            this.groundLayers.sort((l1:number,l2:number)=>l1<l2?-1:l1===l2?0:1);
            const index = this.groundLayers.indexOf(layer);
            this.node.insertChild(panel,index);
            //add tile nodes
            const nodes = this._addTileNodes(panel,create,0);
            //pack
            return [panel,nodes];
        });
    }

    removeGroundTiles(layer:number):[Node,KMap<KKP2D,Node>]{
        const pack = this.groundTileNodesMap.get(layer,true);
        if(pack){
            const [panel,nodes] = pack;
            panel.removeFromParent();
        }
        return pack;
    }

    addCustomTiles(create:()=>Node,layer:number):KMap<KKP2D,Node>{
        return this.customTileNodesMap.computeIfAbsent(layer,()=>{
            return this._addTileNodes(this.customNode,create,layer);
        });
    }

    removeCustomTiles(layer:number):KMap<KKP2D,Node>{
        const nodeMap = this.customTileNodesMap.get(layer,true);
        if(nodeMap){
            nodeMap.foreach((grid,node)=>{
                node.removeFromParent();
            })
        }
        return nodeMap;
    }

    addObj(name:string,position:KKP2D,create:()=>Node):Node{
        const node = this.customObjNodeMap.computeIfAbsent(name,()=>{
            const node = create();
            this.customNode.addChild(node);
            return node;
        });
        let mapPos = node.getComponent(MapPosition);
        if(!mapPos){
            mapPos = node.addComponent(MapPosition);
        }
        mapPos.position = position;
        return node;
    }

    removeObj(name:string):Node{
        const node = this.customObjNodeMap.get(name,true);
        if(node){
            node.removeFromParent();
        }
        return node;
    }

    _moveTo(newGrid:KKP2D,oldGrid:KKP2D,newPos:KKP2D){
        this.viewGrid.grid = newGrid;
        this.viewPosition.position = newPos;
        if(newGrid.x!==oldGrid.x||newGrid.y!==oldGrid.y){
            this.groundTileNodesMap.foreach((layer,[panel,nodeMap])=>{
                this._triggerTileChange(nodeMap);
            });
            this.customTileNodesMap.foreach((layer,nodeMap)=>{
                this._triggerTileChange(nodeMap);
            });
        }
    }

    _moveView(delta:KKP2D){
        const oldGrid = this.viewGrid.grid;
        const oldPos = this.viewPosition.position;
        const [newGrid,newPos] = this.gridForm.combine({
            x: oldGrid.x,
            y: oldGrid.y
        },{
            x: oldPos.x+delta.x,
            y: oldPos.y+delta.y
        });
        this._moveTo(newGrid,oldGrid,newPos);
    }

    _viewAt(target:KKP2D){
        const oldGrid = this.viewGrid.grid;
        const [newGrid,newPos] = this.gridForm.fromPixel(target);
        this._moveTo(newGrid,oldGrid,newPos);
    }

    fromPixel(src:KKP2D):KKP2D{
        return this.gridForm.fromPixel(src)[0];
    }

    toPixel(src:KKP2D):KKP2D{
        return this.gridForm.toPixel(src);
    }

    roundPixel(src:KKP2D):KKP2D{
        return this.toPixel(this.fromPixel(src));
    }

    init() {
        //grid form
        switch(this.form){
            case "orthogonal":
                this.gridForm = new GridFormOrthogonal(this.tileWidth,this.tileHeight);
                break;
            case "rhomboid":
                this.gridForm = new GridFormRhomboid(this.tileWidth,this.tileHeight);
                break;
            case 'hexagonal':
                this.gridForm = new GridFormHexagonal(this.tileWidth,this.tileHeight,this.tileSide);
                break
        }
        const [initGrid,initPos] = this.gridForm.combine({
            x: this.initGridX,
            y: this.initGridY
        },{
            x: this.initX,
            y: this.initY
        });
        //view grid
        this.viewGrid = this.addComponent(MapGrid);
        this.viewGrid.grid = initGrid;
        //view position
        this.viewPosition = this.addComponent(MapPosition);
        this.viewPosition.position = initPos;
        //panel nodes
        this.customNode = buildUINodeEx();
        this.node.addChild(this.customNode);
        this.groundLayers.push(100);
        //register handlers
        this.controller = this.getComponent(MapController);
        if(this.controller){
            this.controller.onMoveView((delta)=>this._moveView(delta));
            this.controller.onViewAt((target)=>this._viewAt(target));
            this.controller.onRoundToGrid((src)=>this.roundPixel(src));
        }
    }

    count = 0;
    tmpSortCost = 0;
    tmpUpdateCost = 0;

    render() {
        this.count++;
        let t0 = 0;
        let t1 = 0;
        //update z index
        t0 = performance.now();
        this.customNode.sortChildren((nodes:Node[])=>{
            //distinguish visible or not
            const visList = new KList<Tile>();
            const unvisList = new KList<Node>();
            const tileArray:Tile[] = [];
            for(const node of nodes){
                let grid = KK_ZERO;
                let offset = KK_ZERO;
                let size = UNIT_SIZE;
                let layer = 100;
                let vis = false;
                //compare pixel
                const mapGrid = node.getComponent(MapGrid);
                if(mapGrid){
                    grid = mapGrid.calcGrid();
                    layer = mapGrid.layer;
                }
                const pos = node.getComponent(MapPosition);
                if(pos){
                    [grid,offset] = this.gridForm.fromPixel(pos.calculate());
                    vis = true;
                }
                const tileCtrl = node.getComponent(TileController);
                if(tileCtrl){
                    size = tileCtrl.getSize();
                    vis = tileCtrl.isVisible();
                }
                if(this.checkSize){
                    if(vis){
                        const edge = this.gridForm.edge(grid,size);
                        visList.push({
                            node: node,
                            grid: grid,
                            size: size,
                            edge: edge,
                            offset: offset,
                            layer: layer
                        });
                    }
                    else{
                        unvisList.push(node);
                    }
                }
                else {
                    tileArray.push({
                        node: node,
                        grid: grid,
                        size: size,
                        edge: EMPTY_EDGE,
                        offset: offset,
                        layer: layer
                    });
                }
            }
            if(this.checkSize){
                //add unvisible
                let index = 0;
                unvisList.foreach((node)=>{
                    nodes[index] = node;
                    index++;
                });
                //check covers
                const coverMap = new KNumTree<{covers:KNumTree<void>,tiles:KList<Tile>}>();
                while(visList.size>0){
                    const tile = visList.shift()!;
                    const covers = new KNumTree<void>();
                    const tss = new KList<KList<Tile>>();
                    tile.edge.foreach((x)=>{
                        //add this
                        covers.set(x);
                        //check covers and tiles
                        const ct = coverMap.get(x,true);
                        if(ct){
                            //save tiles
                            tss.push(ct.tiles);
                            //clear covered records, and re-add
                            ct.covers.foreach((sx)=>{
                                coverMap.get(sx,true);
                                covers.set(sx);
                            })
                        }
                    });
                    const tiles = new KList<Tile>();
                    tss.foreach((ts)=>{
                        tiles.concatBefore(ts,null);
                    });
                    //add this
                    tiles.push(tile);
                    //set all covered
                    const coverTile = {
                        covers: covers,
                        tiles: tiles
                    };
                    covers.foreach((c)=>{
                        coverMap.set(c,coverTile);
                    });
                }
                //sort and add
                while(coverMap.size>0){
                    const coverTile = coverMap.getFirst(true)!.value;
                    //remove same
                    coverTile.covers.foreach((x)=>{
                        coverMap.get(x,true);
                    });
                    //sort
                    approxSort(coverTile.tiles,(t1,t2)=>{
                        const approx = this.gridForm.compareEdge(t1.edge,t2.edge);
                        if(!approx.trust||approx.result!==0){
                            //not intersect
                            return approx.result;
                        }
                        //same or intersect
                        return compareLayerOffset(t1,t2);
                    });
                    coverTile.tiles.foreach((tile)=>{
                        if(index<nodes.length){
                            nodes[index] = tile.node;
                        }
                        index++;
                    });
                }
            }
            else{
                tileArray.sort((t1,t2)=>{
                    // DEBUG
                    // if(t1.layer===100||t2.layer===100){
                    // if((t1.grid.x!==t2.grid.x||t1.grid.y!==t2.grid.y)&&(t1.grid.x===47&&t1.grid.y===85||t2.grid.x===47&&t2.grid.y===85)){
                    //     let log = "tile 1: ("+t1.grid.x+","+t1.grid.y+") ("+t1.size.width+","+t1.size.height+") "+t1.layer+".";
                    //     log += " tile 2: ("+t2.grid.x+","+t2.grid.y+") ("+t2.size.width+","+t2.size.height+") "+t2.layer+".";
                    //     console.log(log);
                    // }
                    const z = this.gridForm.compareZIndex(t1.grid,t2.grid);
                    return z===0?compareLayerOffset(t1,t2):z;
                });
                for(let i=0; i<nodes.length; i++){
                    nodes[i] = tileArray[i].node;
                }
            }
        });
        t1 = performance.now();
        this.tmpSortCost += t1-t0;
        //update position
        t0 = t1;
        const grid = this.viewGrid.grid;
        const pos = this.viewPosition.position;
        this.node.setPosition(-pos.x,-pos.y,0);
        const viewPixel = this.gridForm.toPixel(grid);
        this.customObjNodeMap.foreach((name,node)=>{
            const mapPos = node.getComponent(MapPosition);
            if(mapPos){
                const relative = subP2D(mapPos.position,viewPixel);
                node.setPosition(relative.x,relative.y,0);
            }
        });
        t1 = performance.now();
        this.tmpUpdateCost += t1-t0;
        if(this.count%1000===0){
            console.log("sort: "+this.tmpSortCost+", update: "+this.tmpUpdateCost);
        }
    }
}
